<?php
namespace app\common\controller;

class RedLock
{
    private $retryDelay;
    private $retryCount;
    private $clockDriftFactor = 0.01;

    private $quorum;

    private $servers = array();
    private $instances = array();

    function __construct(array $servers = [], $retryDelay = 200, $retryCount = 3)
    {
        if(empty($servers)){
            $servers[] = [config('redis.host'),config('redis.port'),0.01];
        }

        $this->servers = $servers;

        $this->retryDelay = $retryDelay;
        $this->retryCount = $retryCount;

        $this->quorum  = min(count($servers), (count($servers) / 2 + 1));
    }

    public function lock($resource, $ttl)
    {
        $this->initInstances();

        $token = uniqid();
        $retry = $this->retryCount;//重试次数 3

        do {
            $n = 0;

            $startTime = microtime(true) * 1000;

            //遍历分布式架构中的每个服务器的redis实例
            foreach ($this->instances as $instance) {
                //给每个服务器的redis实例都设置一个只有不存在时才可设置的锁 10000毫妙->10秒
                if ($this->lockInstance($instance, $resource, $token, $ttl)) {
                    $n++;//成功设置锁的个数(获得锁)
                }
            }

            # Add 2 milliseconds to the drift to account for Redis expires
            # precision, which is 1 millisecond, plus 1 millisecond min drift
            # for small TTLs.
            $drift = ($ttl * $this->clockDriftFactor) + 2;
            //有效时间
            $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;

            //判断成功获得锁的个数是否比允许的最少个数多(牺牲强一致性来换取高可用性)
            //如果只有一个服务器 那么就会仅确保强一致性
            //判断锁是否还有剩余的有效时间
            if ($n >= $this->quorum && $validityTime > 0) {
                //TODO 自动续期机制

                //返回锁的键值以及有效时间
                return [
                    'validity' => $validityTime,
                    'resource' => $resource,
                    'token'    => $token,
                ];

            } else {// 不满足条件 则 释放前面获得的所有锁 -- 使用了LUA脚本释放锁
                foreach ($this->instances as $instance) {
                    $this->unlockInstance($instance, $resource, $token);
                }
            }

            //等待随机延迟后再重试(容错性)
            $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
            usleep($delay * 1000);//延迟0.1~0.2s

            $retry--;

        } while ($retry > 0);

        return false;
    }

    public function unlock(array $lock)
    {
        $this->initInstances();
        $resource = $lock['resource'];
        $token    = $lock['token'];
        foreach ($this->instances as $instance) {
            $this->unlockInstance($instance, $resource, $token);
        }
    }

    private function initInstances()
    {
        if (empty($this->instances)) {
            foreach ($this->servers as $server) {
                list($host, $port, $timeout) = $server;
                $redis = new \Redis();
                $redis->connect($host, $port, $timeout);

                $this->instances[] = $redis;
            }
        }
    }

    private function lockInstance($instance, $resource, $token, $ttl)
    {
        return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
    }

    private function unlockInstance($instance, $resource, $token)
    {
        //确保token与持锁客户端获得的token一致才可以释放
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$resource, $token], 1);
    }
}